查看原文
其他

精通Linux系列三十五:使用Shell脚本编程

拾叁 更AI 2023-10-21

点击关注公众号,AI&编程干货及时送达   


使用Shell脚本编程

之前当我们讲到shell(即bash)时,我们提到它内置了一个编程语言。事实上,你可以编写程序或者称之为shell脚本,来完成单一命令无法完成的任务。命令reset-lpg,在书中的示例目录中提供,是一个你可以阅读的shell脚本:

→ less ~/linuxpocketguide/reset-lpg

像任何好的编程语言,shell有变量、条件语句(如if-then-else)、循环、输入和输出等功能。关于shell脚本编程,已经有很多书籍。因此,我们仅提供最基础的内容帮助你入门。要查看完整文档,运行info bash,或者在网上搜索,或者查阅更深入的O’Reilly书籍,如《学习bash Shell》《Bash口袋参考手册》

创建和运行Shell脚本

要创建一个shell脚本,只需将bash命令放入一个文件中,就像你直接输入它们那样。要运行脚本,你有三个选择:

  • • 在文件前添加#!/bin/bash并使文件可执行这是运行脚本的最常见方法。在脚本文件的顶部添加行:#!/bin/bash。它必须是文件的第一行,左对齐。然后使文件可执行:→ **chmod +x myscript**。可以选择将其移动到搜索路径中的一个目录。然后像运行其他命令那样运行它:→ **myscript**。如果脚本位于你当前的目录,但当前目录"."不在你的搜索路径中,你需要加上"./",这样shell才能找到脚本:→ **./myscript**。出于安全原因,当前目录通常不在你的搜索路径中。(你不会希望一个本地脚本(例如)“ls”覆盖真正的ls命令。)

  • • 传递给bashbash会将其参数解释为脚本的名称并运行它。→ **bash myscript**

  • • 使用“.”或source在当前shell中运行上述方法将你的脚本作为一个独立的实体运行,它不会影响到你当前的shell[24]。如果你希望你的脚本能够修改当前的shell(设置变量、更改目录等),可以使用source或“.”命令在当前shell中运行它:→ **. myscript** → **source myscript**

空白和换行

Bash shell脚本对空格和换行非常敏感。因为这个编程语言的“关键词”实际上是由shell解析的命令,所以你需要用空格分隔参数。同样,命令中的换行会误导shell,使其认为命令是不完整的。按照我们在这里呈现的约定,你应该没问题。

如果你必须将一个长命令分成多行,则每行(除最后一行外)都以单个\字符结束,意味着“继续下一行”:

→ grep abcdefghijklmnopqrstuvwxyz file1 file2 \
  file3 file4

变量

我们之前描述过shell变量:

→ MYVAR=6
→ echo $MYVAR
6

所有保存在变量中的值都是字符串,但如果它们是数字,当需要时shell会将它们视为数字:

→ NUMBER="10"
→ expr $NUMBER + 5
15

在shell脚本中引用变量的值时,最好用双引号包围它,以防止某些运行时错误。未定义的变量,或其值中带有空格的变量,如果不用引号包围,会产生意外的值,导致脚本出错:

→ FILENAME="我的文档"            名称中有空格
→ ls $FILENAME                      尝试列出它
ls: 我的: 没有这样的文件或目录   ls看到2个参数
ls: 文档: 没有这样的文件或目录
→ ls -l "$FILENAME"         正确地列出它
我的文档                 ls只看到1个参数

如果一个变量名与另一个字符串相邻评估,为了防止意外的行为,最好用大括号包围它:

→ HAT="fedora(原文)"
→ echo "The plural of $HAT is $HATs"
fedora的复数是            没有"HATs"这个变量
→ echo "The plural of $HAT is ${HAT}s"
fedora的复数是fedoras    这是我们想要的

输入与输出

脚本输出由echoprintf命令提供,我们在[“屏幕输出”]中描述过:

→ echo "Hello world"
你好,世界
→ printf "I am %d years old\n" `expr 20 + 20`
我今年40岁了

输入是由read(原文)命令提供的,它从标准输入读取一行并存储在一个变量中:

→ read name
Sandy Smith <回车>
→ echo "我读到的名字是 $name"
我读到的名字是 Sandy Smith

布尔值和返回码

在我们描述条件语句和循环之前,我们需要解释布尔(真/假)测试的概念。对于shell,值0表示真或成功,其他任何值都表示假或失败。(可以把零看作“没有错误”,其他值看作错误码。)

此外,每个Linux命令在退出时都会返回一个整数值,称为返回码或退出状态,给shell。

您可以在特殊变量$?中看到这个值:

→ cat myfile
我的名字是 Sandy Smith,我真的很喜欢 Ubuntu Linux(原文)
→ grep Smith myfile
我的名字是 Sandy Smith    找到了匹配项...
→ echo $?
0                             ...所以返回码是“成功”
→ grep aardvark myfile
→ echo $?                     没有找到匹配项...
1                             ...所以返回码是“失败”

命令的返回码通常在其manpage上有文档记录。

test 和 “[”

test命令(内置于shell)将评估涉及数字和字符串的简单布尔表达式,并将其退出状态设置为0 (真) 或 1 (假):

→ test 10 -lt 5     10小于5吗?
→ echo $?
1                   不,不是
→ test -n "hello"   “hello”长度非零吗?
→ echo $?
0                   是的,是非零的

以下是用于检查整数、字符串和文件属性的常见test(test)参数:

文件测试
-d name文件 name 是一个目录
-f name文件 name 是一个普通文件
-L name文件 name 是一个符号链接
-r name文件 name 存在且可读
-w name文件 name 存在且可写
-x name文件 name 存在且可执行
-s name文件 name 存在且其大小非零
f1 -nt f2文件 f1 比文件 f2 新
f1 -ot f2文件 f1 比文件 f2 旧
字符串测试
s1 = s2字符串 s1 等于字符串 s2
s1 != s2字符串 s1 不等于字符串 s2
-z s1字符串 s1 长度为零
-n s1字符串 s1 长度非零
数字测试
a -eq b整数 a 和 b 相等
a -ne b整数 a 和 b 不相等
a -gt b整数 a 大于整数 b
a -ge b整数 a 大于或等于整数 b
a -lt b整数 a 小于整数 b
a -le b整数 a 小于或等于整数 b
组合和否定测试
t1 -a t2与: 测试 t1 和 t2 都为真
t1 -o t2或: 测试 t1 或 t2 是真的
! your_test否定测试 (即,your_test(your_test)为假)
\( your_test \)括号用于分组,如同代数

test(test)有一个不寻常的别名,“[” (左方括号),用作与条件和循环的简写。如果你使用这个简写,你必须提供一个最终的参数“]” (右方括号)来标示测试的结束。以下测试与之前的两个相同:

→ [ 10 -lt 5 ]
→ echo $?
1
→ [ -n "hello" ]
→ echo $?
0

请记住,“[”就像其他命令一样,因此其后跟的是由空格分隔的单独参数。所以如果你误忘了一些空格:

→ [ 5 -lt 4]          4和]之间没有空格
bash: [: missing ']'

那么test(test)会认为最后的参数是字符串“4]”并抱怨最后的括号丢失了。

一个更强大但不那么便携的布尔测试语法是双括号,[[,它增加了正则表达式匹配并消除了test的一些怪癖。详细内容请参见http://mywiki.wooledge.org/BashFAQ/031

条件语句

if语句用于选择不同的选项,每个选项可能有复杂的测试条件。最简单的形式是if-then语句:

if 命令          如果命令的退出状态为0
then
  主体
fi

这里有一个带有if语句的示例脚本:

→ cat 脚本-if(英文:script-if)
#!/bin/bash
if [ `whoami` = "root" ]
then
  echo "你是超级用户"
fi

接下来是if-then-else语句:

if 命令
then
  主体1
else
  主体2
fi

例如:

→ cat 脚本-else(英文:script-else)
#!/bin/bash
if [ `whoami` = "root" ]
then
  echo "你是超级用户"
else
  echo "你只是个普通人"
fi
→ ./脚本-else(英文:script-else)
你只是个普通人
→ sudo ./脚本-else(英文:script-else)
密码:********
你是超级用户

最后,我们有if-then-elif-else形式,你可以有任意多的测试:

if 命令1
then
  主体1
elif 命令2
then
  主体2
elif ...
  ...
else
  主体N
fi

例如:

→ cat 脚本-elif(英文:script-elif)
#!/bin/bash
bribe=20000
if [ `whoami` = "root" ]
then
  echo "你是超级用户"
elif [ "$USER" = "root" ]
then
  echo "你可能是超级用户"
elif [ "$bribe" -gt 10000 ]
then
  echo "你可以付费成为超级用户"
else
  echo "你还只是个普通人"
fi
→ ./脚本-elif(英文:script-elif)
你可以付费成为超级用户

case语句评估一个单一的值并转到适当的代码片段:

→ cat 脚本-case(英文:script-case)
#!/bin/bash
echo -n "你想做什么(吃东西, 睡觉)?"
read answer
case "$answer" in
  eat)
    echo "好的,来个汉堡。"
    ;;
  sleep)
    echo "那么,晚安。"
    ;;
  *)
    echo "我不确定你想做什么。"
    echo "我想我们明天再见。"
    ;;
esac
→ ./脚本-case(英文:script-case)
你想做什么(吃东西, 睡觉)?睡觉
那么,晚安。

一般的形式是:

case 字符串 in
  表达式1)
    主体1
    ;;
  表达式2)
    主体2
    ;;
  ...
  表达式N)
    主体N
    ;;
  *)
    其他主体
    ;;
esac

其中*字符串是任何值,通常是一个变量值,如$myvar表达式1表达式N*都是模式(运行info bash命令以获取详情),最后的*类似于最终的"else"。每组命令必须由;;终止(如下所示):

→ cat 脚本-case2(英文:script-case2)
#!/bin/bash
echo -n "输入一个字母:"
read letter
case $letter in
  X)
    echo "$letter 是 X"
    ;;
  [aeiou])
    echo "$letter 是个元音"
    ;;
  [0-9])
    echo "$letter 是个数字,真傻"
    ;;
  *)
    echo "字母'$letter' 不被支持"
    ;;
esac
→ ./脚本-case2(英文:script-case2)
输入一个字母:e
e 是个元音

循环

while循环会在某个条件为真的情况下重复一组命令。

while 命令       当命令的退出状态为0时
do
  主体
done

例如:

→ cat script-while
#!/bin/bash
i=0
while [ $i -lt 3 ]
do
  echo "$i"
  i=`expr $i + 1`
done
→ ./script-while
0
1
2

until循环会重复直到某个条件变为真:

until 命令   当命令的退出状态为非零时
do
  主体
done

例如:

→ cat script-until
#!/bin/bash
i=0
until [ $i -ge 3 ]
do
  echo "$i"
  i=`expr $i + 1`
done
→ ./script-until
0
1
2

小心避免无限循环,使用while的条件始终评估为0(真),或until的条件始终评估为非零值(假):

i=1
while [ $i -lt 10 ]    变量i永远不会改变。这是无限的!
do
  echo "forever"
done

另一种类型的循环,for循环,遍历来自列表的值:

for 变量 in 列表
do
  主体
done

例如:

→ cat script-for
#!/bin/bash
for name in Tom Jane Harry
do
  echo "$name是我的朋友"
done
→ ./script-for
Tom是我的朋友
Jane是我的朋友
Harry是我的朋友

for循环特别适用于处理文件列表;例如,当前目录中具有某个扩展名的文件名:

→ cat script-for2
#!/bin/bash
for file in *.docx
do
  echo "$file是一个臭名昭著的Microsoft Word文件(Microsoft Word file)"
done
→ ./script-for2
letter.docx是一个臭名昭著的Microsoft Word文件

您还可以使用seq命令(查看[seq])产生一系列连续的整数,然后遍历这些数字:

→ cat script-seq
#!/bin/bash
for i in $(seq 1 20)   生成数字1 2 3 4 ... 20
do
  echo "迭代$i"
done
→ ./script-seq
迭代1
迭代2
迭代3
...
迭代20

命令行参数

Shell脚本可以像其他Linux命令一样接受命令行参数和选项。(实际上,一些常见的Linux命令就是脚本。)在您的shell脚本中,您可以将这些参数引用为$1$2$3等:

→ cat script-args
#!/bin/bash
echo "我的名字是$1,我来自$2"

→ ./script-args Johnson Wisconsin
我的名字是Johnson,我来自Wisconsin
→ ./script-args Bob
我的名字是Bob,我来自

您的脚本可以使用$#测试它接收的参数数量:

→ cat script-args2
#!/bin/bash
if [ $# -lt 2 ]
then
  echo "$0错误:您必须提供两个参数"
else
  echo "我的名字是$1,我来自$2"
fi

特殊值$0包含脚本的名称,对于使用和错误消息很有用:

→ ./script-args2 Barbara
./script-args2错误:您必须提供两个参数

要遍历所有命令行参数,请使用for循环和特殊变量$@,它包含所有参数:

→ cat script-args3
#!/bin/bash
for arg in $@
do
  echo "我找到了参数$arg"
done
→ ./script-args3 One Two Three
我找到了参数One
我找到了参数Two
我找到了参数Three

使用返回码退出

exit命令终止您的脚本并将给定的返回码传递给shell。按照传统,脚本应该返回0表示成功,返回1(或其他非零值)表示失败。如果您的脚本没有调用exit,返回码自动为0:

→ cat script-exit
#!/bin/bash
if [ $# -lt 2 ]
then
  echo "$0错误:您必须提供两个参数"
  exit 1
else
  echo "我的名字是$1,我来自$2"
fi
exit 0

→ ./script-exit Bob
./script-exit错误:您必须提供两个参数
→ echo $?
1

传输给bash

Bash不仅仅是一个shell;它也是一个命令,bash,它从标准输入中读取。这意味着你可以构建命令作为字符串,并将它们发送给bash执行:

→ echo wc -l myfile
wc -l myfile
→ echo wc -l myfile | bash
18 myfile

BASH警告

将命令传输到bash非常强大,但也可能非常危险。首先确保你确切地知道哪些命令将被执行。你不希望意外地把rm命令传输到bash并删除一个有价值的文件(或1,000个有价值的文件)。

如果有人让你检索一个网页(例如,使用curl命令)并盲目地传输到bash,请不要这样做!相反,将网页作为一个文件捕获(使用curlwget),仔细检查它,然后做出明智的决定是否使用bash执行它。

这种技术非常有用。假设你想从一个网站下载文件photo1.jpgphoto2.jpg,直到photo100.jpg。而不是手动输入100个wget命令,用循环构建命令,使用seq构建从1到100的整数列表:

→ for i in `seq 1 100`
do
  echo wget http://example.com/photo$i.jpg
done
wget http://example.com/photo1.jpg
wget http://example.com/photo2.jpg
...
wget http://example.com/photo100.jpg

是的,你已经构建了100条命令的文本。现在把输出传输给bash,它会运行所有100条命令,就像你手动输入它们一样:

→ for i in `seq 1 100`
do
  echo wget http://example.com/photo$i.jpg
done | bash

这里有一个更复杂但实用的应用。假设你有一组文件想要重命名。将旧名称放入文件oldnames中,新名称放入newnames文件中:

→ cat oldnames
oldname1
oldname2
oldname3
→ cat newnames
newname1
newname2
newname3

现在使用pastesed命令(“文件文本操作”)将旧的和新的名称并排放置,并在每一行前加上"mv",输出结果是一系列的“mv”命令:

→ cat oldnames | paste -d' ' oldnames newnames \
  | sed 's/^/mv /'
mv oldfile1 newfile1
mv oldfile2 newfile2
mv oldfile3 newfile3

最后,将输出传输给bash,重命名就发生了!

→ cat oldnames | paste -d' ' oldnames newnames \
  | sed 's/^/mv /' \
  | bash

拓展

Shell脚本对许多目的来说都很好,但Linux还配备了更强大的脚本语言,以及编译型编程语言。以下是其中的一些:

语言程序入门方法...
C, C++gccg++man gcc[https://gcc.gnu.org/]
.NETmonoman mono[http://www.mono-project.com/]
Javajavac[http://java.com/]
Perlperlman perl[http://www.perl.com/]
PHPphpman php[http://php.net/]
Pythonpythonman python[https://www.python.org/]
Rubyruby[http://www.ruby-lang.org/]

推荐阅读

··································

你好,我是拾叁,7年开发老司机、互联网两年外企5年。怼得过阿三老美,也被PR comments搞崩溃过。这些年我打过工,创过业,接过私活,也混过upwork。赚过钱也亏过钱。一路过来,给我最深的感受就是不管学什么,一定要不断学习。只要你能坚持下来,就很容易实现弯道超车!所以,不要问我现在干什么是否来得及。如果你还没什么方向,可以先关注我,这里会经常分享一些前沿资讯和编程知识,帮你积累弯道超车的资本。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存